//
//  MyDocument.m
//  iPhone Backup Slurp
//
//  Created by Bob Newhart on 10/15/08.
//  Copyright Geekspiff 2008 . All rights reserved.
//

#import "MyDocument.h"
#import "OSLDatabaseController.h"
#import <QTKit/QTKit.h>

@implementation MyDocument
@synthesize attributedContents = _attributedContents;

- (NSString *)windowNibName
{  return @"MyDocument";  }

- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
    [super windowControllerDidLoadNib:aController];
}

- (void)createReadError: (NSError **)outError descriptionKey: (NSString *)descriptionKey format: (NSString *)format
{
	NSString *description = [NSString stringWithFormat: NSLocalizedString(descriptionKey, nil), format];
	NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
							  NSLocalizedString(@"ReadFailHeader", nil),	NSLocalizedDescriptionKey, 
							  description ,									NSLocalizedRecoverySuggestionErrorKey, 
							  nil];
	*outError = [NSError errorWithDomain: NSCocoaErrorDomain code: 0 userInfo: userInfo];
}

- (NSString *)temporaryFilePath
{
	NSString *path = nil;
	CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
	if (uuid) {
		CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuid);
		if (uuidString) {
			path = [NSTemporaryDirectory() stringByAppendingPathComponent: (NSString *)uuidString];
			CFRelease(uuidString);
		}
		CFRelease(uuid);
	}
	
	return path;
}

- (BOOL)_handleSQLite3: (NSData *)data error: (NSError **)outError
{
	OSLDatabaseController *db = [[[OSLDatabaseController alloc] initWithDatabasePath: _path error: outError] autorelease];
	if (!db) {
		[self appendContents: [NSString stringWithFormat: @"%@\n\n", NSLocalizedString(@"UnreadableSQLite", nil)]];
		[self appendFileAttachment: data name: [_saveFileName lastPathComponent]];
		return YES;
	}
	
	[self appendContents: @"SQLite 3 master table:\n\n"];

	NSMutableArray *tables = [NSMutableArray array];
	NSString *sql = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;";
	[db executeSQL: sql withCallback: ReadDictionariesCallback context: tables];
	[self appendContents: [tables description]];

	for (NSDictionary *tableDictionary in tables) {
		NSString *name = [tableDictionary valueForKey: @"name"];
		if (!name) continue;
		
		[self appendContents: [NSString stringWithFormat: @"\n\n%@\n", name]];
		
		NSMutableArray *tableData = [NSMutableArray array];
		sql = [NSString stringWithFormat: @"SELECT * FROM %@;", name];
		[db executeSQL: sql withCallback: ReadDictionariesCallback context: tableData];
		[self appendContents: [tableData description]];
	}
	
	return YES;
}

- (BOOL)_handleDynamicDictionary: (NSData *)data error: (NSError **)outError
{
	[self appendContents: @"DynamicDictionary-3\n"];
	void const *bytes = [data bytes];
	size_t length = [data length];
	char const header[] = "DynamicDictionary-3";
	size_t headerLength = strlen(header);
	NSParameterAssert( length >= headerLength + sizeof(uint32_t) );
	
	void const *headerPtr = (void const *) ((intptr_t)bytes + headerLength + 1);
	size_t i, entryCount = CFSwapInt32HostToBig(*(uint32_t *)headerPtr);
	[self appendContents: [NSString stringWithFormat: @"Dumping %u entries: {\n", entryCount]];
	
	char const *entry = (char const *) ((intptr_t)headerPtr + sizeof(uint32_t));
	for (i = 0; i < entryCount; ++i) {
		NSString *entryString = [NSString stringWithUTF8String: entry];
		[self appendContents: [NSString stringWithFormat: @"\t\"%@\"\n", entryString]];
		entry += [entryString length] + 1;
	}
	[self appendContents: @"}\n"];
		
	return YES;
}

- (BOOL)_handlePList: (NSData *)data binary: (BOOL)binary error: (NSError **)outError
{
	[self appendContents: [NSString stringWithFormat: @"%@ plist:\n\n", binary ? @"Binary" : @"ASCII"]];
	NSString *errorString = nil;
	NSDictionary *plist = [NSPropertyListSerialization propertyListFromData: data mutabilityOption: NSPropertyListImmutable format: NULL errorDescription: &errorString];
	if (!plist) {
		[self createReadError: outError descriptionKey: @"ReadFailBadPList" format: errorString];
		return NO;
	}
	
	[self appendContents: [plist description]];
	return YES;
}

- (BOOL)_handleZip: (NSData *)data error: (NSError **)outError
{
	[self appendContents: @"Zipped filenames:\n\n"];

	NSTask *unzipTask = [[[NSTask alloc] init] autorelease];
	NSPipe *outputPipe = [NSPipe pipe];
	NSFileHandle *outHandle = [outputPipe fileHandleForReading];
	
	[unzipTask setLaunchPath:@"/usr/bin/zipinfo"];
	[unzipTask setStandardOutput: outputPipe];
	
	NSArray *arguments = [NSArray arrayWithObject: _path];
	[unzipTask setArguments:arguments];
	[unzipTask launch];
	
	// block until task completes
	while ([unzipTask isRunning])
		[NSThread sleepUntilDate:[NSDate distantPast]];
	
	if (0 != [unzipTask terminationStatus]) {
		[self createReadError: outError descriptionKey: @"ZipInfoFailed" format: [NSString stringWithFormat: @"%d", [unzipTask terminationStatus]]];
		return NO;
	}
	
	NSData *outData = [outHandle readDataToEndOfFile];
	NSParameterAssert( outData && 0 != [outData length] );
	NSMutableString *contents = [[[NSMutableString alloc] initWithData: outData encoding: NSUTF8StringEncoding] autorelease];
	NSRange firstLine = [contents lineRangeForRange: NSMakeRange(0, 1)];
	if (NSNotFound != firstLine.location) 
		[contents deleteCharactersInRange: firstLine];
	[self appendContents: contents];
	
	[self appendContents: @"\n\n"];
	[self appendFileAttachment: data name: [_saveFileName lastPathComponent]];

	return YES;
}

- (BOOL)_handleString: (NSData *)data
{
	NSString *contents = [[[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding] autorelease];
	if (!contents) return NO;
	
	[self appendContents: @"Generic String:\n\n"];
	[self appendContents: contents];
	return YES;
}

- (BOOL)_handleImage: (NSData *)data
{
	CGImageRef image = NULL;
	CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)data, NULL);
	if (imageSource) {
		NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
								 (id)kCFBooleanTrue, 	(id)kCGImageSourceCreateThumbnailFromImageAlways,
								 (id)kCFBooleanTrue, 	(id)kCGImageSourceCreateThumbnailWithTransform,
								 nil];
		image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (CFDictionaryRef)options);
		CFRelease(imageSource);
	}
	
	if (!image) return NO;
	
	NSBitmapImageRep *bitmap = [[[NSBitmapImageRep alloc] initWithCGImage: image] autorelease];
	CFRelease(image);
	NSSize imageSize = {[bitmap pixelsWide], [bitmap pixelsWide]};
	NSImage *nsImage = [[[NSImage alloc] initWithSize: imageSize] autorelease];
	[nsImage addRepresentation: bitmap];
	
	[self appendAttachment: nsImage];
	return YES;
}

- (BOOL)_handleMovie: (NSData *)data error: (NSError **)outError
{
	id movie = [QTMovie movieWithFile: _path error: outError];
	if (!movie) return NO;
	
	[self appendAttachment: movie];
	return YES;
}

- (BOOL)_handleAudio: (NSData *)data error: (NSError **)outError
{
	NSDictionary *movieAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
									 _path,					QTMovieFileNameAttribute, 
									 (id)kCFBooleanFalse,	QTMovieOpenAsyncOKAttribute, 
									 nil];
	QTMovie *movie = [QTMovie movieWithAttributes:movieAttributes error:outError];
	if (!movie) return NO;
	
	[self appendAttachment: movie];
	return YES;
}

- (BOOL)appendData: (NSData *)data error: (NSError **)outError
{
	static NSSet *sAudioExtensions = nil;
	if (!sAudioExtensions) {
		sAudioExtensions = [[NSSet alloc] initWithObjects: 
							@"mp4", 
							@"m4a", 
							@"m4p", 
							@"mp3", 
							nil];
	}
	
	void const *bytes = [data bytes];
	if (!bytes) {
		[self createReadError: outError descriptionKey: @"ReadFailNoData" format: nil];
		return NO;
	}
	size_t stringLength, length = [data length];
	
	[_path release];
	_path = [[self temporaryFilePath] retain];
	BOOL didSave = [data writeToFile: _path options: 0 error: outError];
	if (!didSave) return NO;
	
	@try {
		char const *sqlDictionary = "SQLite format 3";
		stringLength = strlen(sqlDictionary);
		if (length >= stringLength && 0 == memcmp(sqlDictionary, bytes, stringLength))
			return [self _handleSQLite3: data error: outError];
		
		char const *dynamicDictionary = "DynamicDictionary-3";
		stringLength = strlen(dynamicDictionary);
		if (length >= stringLength && 0 == memcmp(dynamicDictionary, bytes, stringLength))
			return [self _handleDynamicDictionary: data error: outError];
		
		char const *uncompressedPlist = "<?xml version=\"1.0\" encoding=\"";
		stringLength = strlen(uncompressedPlist);
		if (length >= 60 && 0 == memcmp(uncompressedPlist, bytes, stringLength) && NULL != strnstr(bytes, "plist", 59))
			return [self _handlePList: data binary: NO error: outError];
		
		char const *compressedPlist = "bplist";
		stringLength = strlen(compressedPlist);
		if (length >= stringLength && 0 == memcmp(compressedPlist, bytes, stringLength))
			return [self _handlePList: data binary: YES error: outError];
		
		char pkzip[4] = {0x50, 0x4b, 0x03, 0x04};
		if (length > 4 && 0 == memcmp(pkzip, bytes, 4))
			return [self _handleZip: data error: outError];
		
		if ([self _handleString: data]) return YES;
		if ([self _handleImage: data]) return YES;
		
		if ([QTMovie canInitWithFile: _path])
			return [self _handleMovie: data error: outError];
		
		// - Do the pathExtension stuff after checking for a QTMovie so that canInitWithFile doesn't spuriously
		//   return YES;
		
		NSString *pathExtension = [[_saveFileName pathExtension] lowercaseString];
		if (pathExtension && [sAudioExtensions containsObject: pathExtension]) {
			NSError *dummyError;
			NSString *newPath = [_path stringByAppendingPathExtension: pathExtension];
			[[NSFileManager defaultManager] moveItemAtPath: _path toPath: newPath error: &dummyError];
			[_path release];
			_path = [newPath retain];
			return [self _handleAudio: data error: outError];
		}
		
		if (length >= 4) {
			unsigned char firstChar = ((unsigned char *)bytes)[0];
			if (firstChar > 127) {
				unsigned i;
				NSMutableString *hex = [NSMutableString string];
				for (i = 0; i < 4; ++i) {
					unsigned char thisChar = ((unsigned char const *)bytes)[i];
					[hex appendFormat: @"%02x", thisChar];
				}
				[self appendContents: [NSString stringWithFormat: NSLocalizedString(@"ReadFailBinaryData", nil), hex]];
				[self appendContents: @"\n\n"];
				[self appendFileAttachment: data name: [_saveFileName lastPathComponent]];
				return YES;
			}
		}
		
		if (length >= 10) {
			char firstBytes[11];
			memcpy(firstBytes, bytes, 10);
			firstBytes[10] = 0;
			NSString *errorString = [NSString stringWithUTF8String: firstBytes];
			[self appendContents: [NSString stringWithFormat: NSLocalizedString(@"ReadFailUnknownDataTypeLong", nil), errorString]];
			[self appendContents: @"\n\n"];
			[self appendFileAttachment: data name: [_saveFileName lastPathComponent]];
			return YES;
		}
		
		[self appendContents: NSLocalizedString(@"ReadFailUnknownDataTypeShort", nil)];
		[self appendContents: @"\n\n"];
		[self appendFileAttachment: data name: [_saveFileName lastPathComponent]];
	}
	
	@catch (NSException * e) {
		[self createReadError: outError descriptionKey: @"ReadFailException" format: [e description]];
		return NO;
		
	}
	@finally {
		NSError *dummyError;
		[[NSFileManager defaultManager] removeItemAtPath: _path error: &dummyError];
		[_path release];
		_path = nil;
	}
	
	return YES;
}

- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError
{
	NSString *errorString = nil;
	NSDictionary *plist = [NSPropertyListSerialization propertyListFromData: data mutabilityOption: NSPropertyListImmutable format: NULL errorDescription: &errorString];
	if (!plist) {
		[self createReadError: outError descriptionKey: @"ReadFailBadPList" format: errorString];
		return NO;
	}
	
	[_saveFileName release];
	_saveFileName = [[plist valueForKey: @"Path"] retain];
	
	NSMutableDictionary *datalessPlist = [NSMutableDictionary dictionaryWithDictionary: plist];
	[datalessPlist removeObjectForKey: @"Data"];
	[self appendContents: @"Metadata: \n\n"];
	[self appendContents: [datalessPlist description]];
	[self appendContents: @"\n\n"];
	
	NSData *embeddedData = [plist valueForKey: @"Data"];
	if (!embeddedData) {
		[self appendContents: NSLocalizedString(@"NoData", nil)];
		[self appendContents: @"\n\n"];
		[self appendContents: [plist description]];
	}
	
	return [self appendData: embeddedData error: outError];
}

- (void)appendContents: (NSString *)contents
{
	[self willChangeValueForKey: @"attributedContents"];
	if (!_attributedContents)
		_attributedContents = [[NSMutableAttributedString alloc] init];
	
	if (contents) {
		NSAttributedString *attributedContents = [[NSAttributedString alloc] initWithString: contents];
		[_attributedContents appendAttributedString: attributedContents];
		[attributedContents release];
	}
	[self didChangeValueForKey: @"attributedContents"];
}

- (void)appendAttachment: (id)attachedObject
{
	NSTextAttachment *ta = [[[NSTextAttachment alloc] initWithFileWrapper: nil] autorelease];
	[(NSTextAttachmentCell *)[ta attachmentCell] setImage: attachedObject];
	NSAttributedString *as = [NSAttributedString attributedStringWithAttachment: ta];
	
	[self willChangeValueForKey: @"attributedContents"];
	[_attributedContents appendAttributedString: as];
	[self didChangeValueForKey: @"attributedContents"];
}

- (void)appendFileAttachment: (NSData *)data name: (NSString *)name
{
	NSFileWrapper *fw = [[[NSFileWrapper alloc] initRegularFileWithContents: data] autorelease];
	[fw setPreferredFilename: name];
	
	NSTextAttachment *ta = [[[NSTextAttachment alloc] initWithFileWrapper: fw] autorelease];
	NSAttributedString *as = [NSAttributedString attributedStringWithAttachment: ta];
	
	[self willChangeValueForKey: @"attributedContents"];
	[_attributedContents appendAttributedString: as];
	[self didChangeValueForKey: @"attributedContents"];
}

- (NSAttributedString *)attributedContents
{  return [[_attributedContents copy] autorelease];  }

@end
